<template>
  <!-- 索引列表 -->
  <view class="mxp-index-list" :style="[{ height: data.contentHeight }, cusStyle]" :class="cusClass">
    <scroll-view :scrollTop="data.scrollTop" :style="{ height: data.contentHeight }" class="scroll-view" :scrollY="true" @scroll="onScroll">
      <mxp-index-list-item
        v-for="(item, index) in data.indexList"
        :key="index"
        :titleBackground="titleBackground"
        :titleColor="titleColor"
        :titleFontSize="titleFontSize"
        :titlePadding="titlePadding"
        :childListBackground="childListBackground"
        :childListPadding="childListPadding"
        :childListBorderRadius="childListBorderRadius"
        :childItemColor="childItemColor"
        :childItemFontSize="childItemFontSize"
        :childItemPadding="childItemPadding"
        :activeItemColor="themeColor"
        :showSelectedIcon="showSelectedIcon"
        :item="item"
        :selectedList="data.selectedList"
        :disabledList="data.disabledList"
        @itemClick="onItemClick"
      >
        <template #title>
          <slot name="title" :index="index" :item="item"></slot>
        </template>
        <template #item>
          <slot name="item" :index="index" :item="item"></slot>
        </template>
      </mxp-index-list-item>
    </scroll-view>
    <!-- 右边悬浮的索引盒子 -->
    <view :id="indexId" class="index-box" @touchend="touchend" @touchstart="touchstart" @touchmove.stop.passive="touchmove">
      <view
        v-for="(item, index) in data.indexList"
        :key="index"
        class="index"
        :style="{ background: data.currentIndex === index ? themeColor : 'transparent' }"
      >
        <text :style="{ color: data.currentIndex === index ? bubbleTextColor : indexColor }">{{ item.title }}</text>
      </view>
    </view>
    <!-- 气泡盒子 -->
    <view :id="bubbleBoxId" class="bubble-box" :class="data.isTouch ? 'show' : ''" :style="[newBubbleBoxStyle]">
      <text v-if="data.currentIndex >= 0" class="text" :style="{ color: bubbleTextColor }">{{ data.indexList[data.currentIndex]?.title }}</text>
    </view>
  </view>
</template>

<script lang="ts" setup>
  import { type CSSProperties, computed, getCurrentInstance, nextTick, onMounted, reactive, watch } from 'vue';
  import { getDomSize } from '../mxp-tool/utils/dom';
  import type { $AllColorStr, $SizeUnitArr4Str, $SizeUnitStr } from '@/mxp-ui/mxp-tool/types/interface';
  import MxpIndexListItem from './components/mxp-index-list-item.vue';
  import { pinyin } from '@/mxp-ui/mxp-tool/sdk/pinyin-pro/core/pinyin/index';
  import { isObject } from '../mxp-tool/utils/verify';
  import { getOnlyID } from '../mxp-tool/utils/common';
  import { isDarkColorStorage } from '../mxp-tool/utils/color';
  import type { ScrollViewOnScrollEvent, TouchEvent } from 'mxp-uni-types';
  import mxpGetSysInfoStore from '../mxp-tool/store/mxp-store-sys-info';

  export type $IndexListChildItem = { pinyin: string; label: string; value: unknown; index: number; item: any };
  export type $IndexListItem = { title: string; height: number; top: number; data: $IndexListChildItem[] };
  export type $IndexListChange = { currInfo: $IndexListChildItem; selected: any };

  export type $indexListProps = {
    /** 组件高度，一般不用设置，组件会自动撑满一页，如果组件上方或者下方有其他元素可以使用solt传入 */
    height?: $SizeUnitStr | `calc(${string})`;
    /** 是否展示数据为空的索引 */
    showEmptyData?: boolean;
    /** 数据源 */
    list?: unknown[];
    /** 是否为多选 */
    multiple?: boolean;
    /** 是否可以取消选中 */
    uncheck?: boolean;
    /** 标题颜色 */
    titleColor?: $AllColorStr;
    /** 标题背景色 */
    titleBackground?: $AllColorStr;
    /** 标题字体大小 */
    titleFontSize?: $SizeUnitStr;
    /** 标题的边距 如：'10rpx 20rpx' 或者 '10rpx'等等 */
    titlePadding?: $SizeUnitArr4Str;
    /** 子选项列表的背景色 */
    childListBackground?: $AllColorStr;
    /** 子选项列表的边距 如：'10rpx 20rpx' 或者 '10rpx'等等 */
    childListPadding?: $SizeUnitArr4Str;
    /** 子选项列表的圆角 */
    childListBorderRadius?: $SizeUnitStr;
    /** 每个子选项的文字颜色 */
    childItemColor?: $AllColorStr;
    /** 每个子选项的文字大小 */
    childItemFontSize?: $SizeUnitStr;
    /** 每个子选项列表的边距 如：'10rpx 20rpx' 或者 '10rpx'等等 */
    childItemPadding?: $SizeUnitArr4Str;
    /** 右边索引文字的颜色 */
    indexColor?: $AllColorStr;
    /** 主题色(索引字母选中后背景色和选中的颜色) */
    themeColor?: $AllColorStr;
    /** 需要展示属性的 key,不传则同 valueKey, 如 valueKey 也没传则为：'label' */
    labelKey?: number | string;
    /** 需要展示属性的 key,不传则同 labelKey, 如 labelKey 也没传则为：'value' */
    valueKey?: number | string;
    /** 选中的值 */
    modelValue?: unknown[] | boolean | number | object | string;
    /** 禁止点击的值（那些值禁止选中或取消选中） */
    disabledValue?: unknown[];
    /** 选中时是否显示勾选的图标 */
    showSelectedIcon?: boolean;
    /** 自定义的样式属性 */
    bubbleBoxStyle?: CSSProperties;
    /** 自定义的样式属性 */
    cusStyle?: CSSProperties;
    /** 自定义类名 */
    cusClass?: string;
  };
  defineOptions({
    name: 'mxp-index-list',
  });

  const props = withDefaults(defineProps<$indexListProps>(), {
    height: undefined,
    showEmptyData: false,
    list: () => [],
    multiple: false,
    uncheck: true,
    titleColor: '#888888',
    titleBackground: '#ffffff',
    titlePadding: '8px 0px',
    titleFontSize: '14px',
    childListBackground: '#f2f2f2',
    childListPadding: '15px',
    childListBorderRadius: '10px',
    childItemColor: '#999999',
    childItemFontSize: '14px',
    childItemPadding: '10px 0px',
    indexColor: '#888888',
    themeColor: '#223aec',
    labelKey: undefined,
    valueKey: undefined,
    modelValue: undefined,
    showSelectedIcon: false,
    disabledValue: () => [],
    bubbleBoxStyle: () => ({}),
    cusStyle: () => ({}),
    cusClass: '',
  });
  defineSlots<{
    /** 标题 */
    title?: () => void;
    /** 自定义子项 */
    item?: (data: { item: $IndexListItem; index: number }) => void;
  }>();
  const emits = defineEmits<{
    (e: 'change', data: $IndexListChange): void;
    (e: 'update:modelValue', data: any): void;
  }>();
  const sys = mxpGetSysInfoStore().getSysInfo();
  const data = reactive({
    contentHeight: props.height || `${sys.value.windowHeight}px`,
    scrollTop: 0,
    currentIndex: -1,
    searchHeight: '36px' as $SizeUnitStr,
    /** 索引列表数据 */
    indexList: [] as $IndexListItem[],
    /** 是否为手指滑动期间，滑动期间不处理滚动事件 */
    isTouch: false,
    /** 选中的值 */
    selectedList: [] as unknown[],
    /** 禁用的值 */
    disabledList: [] as unknown[],
    indexBoxInfo: {
      /** 子节点高度, */
      childHeight: 0,
      /** 距离顶部的高度, 手指移入实践是从最顶部开始计算的 */
      top: 0,
      /** 移动盒子的高度 */
      moveBoxHeight: 0,
    },
  });

  watch(
    () => props.list,
    () => {
      initData();
    }
  );
  watch(
    () => props.modelValue,
    (val) => {
      data.selectedList = Array.isArray(val) ? val : val === '' || val === undefined ? [] : [val];
    },
    { immediate: true }
  );
  watch(
    () => props.disabledValue,
    (val) => {
      data.disabledList = val;
    },
    { immediate: true }
  );

  const indexId = getOnlyID('index-box');
  const bubbleBoxId = getOnlyID('bubble-box');

  const instance = getCurrentInstance();

  const newBubbleBoxStyle = computed<CSSProperties>(() => {
    const { childHeight, top, moveBoxHeight } = data.indexBoxInfo;
    const calcTop = childHeight * (data.currentIndex + 1) - moveBoxHeight / 2 + top;
    // 减 4 是因为旋转后中心位置变了，所以减4
    const style: CSSProperties = { background: props.themeColor, top: `${calcTop - 4}px`, ...props.bubbleBoxStyle };
    return style;
  });

  const bubbleTextColor = computed(() => (isDarkColorStorage(props.themeColor) ? '#000000' : '#ffffff'));

  const keyObj = computed(() => {
    const haveLabelKey = ['string', 'number'].includes(typeof props.labelKey);
    const haveValueKey = ['string', 'number'].includes(typeof props.valueKey);
    return {
      labelKey: (haveLabelKey ? props.labelKey : haveValueKey ? props.valueKey : 'label') as string,
      valueKey: (haveValueKey ? props.valueKey : haveLabelKey ? props.labelKey : 'value') as string,
    };
  });

  onMounted(() => {
    data.contentHeight = props.height || `${sys.value.windowHeight}px`;
    nextTick(() => {
      initData();
    });
  });

  /**
   * 点击某项
   */
  function onItemClick(child: $IndexListChildItem) {
    const index = data.selectedList.findIndex((item) => item === child.value);
    if (props.multiple) {
      // 多选
      if (index >= 0) {
        props.uncheck && data.selectedList.splice(index, 1);
      } else {
        data.selectedList.push(child.value);
      }
      emits('update:modelValue', data.selectedList);
      emits('change', { currInfo: child, selected: data.selectedList });
    } else {
      // 单选
      if (index >= 0) {
        props.uncheck && data.selectedList.splice(index, 1);
      } else {
        data.selectedList = [child.value];
      }
      emits('update:modelValue', data.selectedList[0]);
      emits('change', { currInfo: child, selected: data.selectedList[0] });
    }
  }

  /**
   * 触摸开始
   */
  function touchstart(event: TouchEvent) {
    data.isTouch = true;
    const pageY = event.changedTouches[0]?.pageY || 0;
    goIndexView(Math.floor((pageY - data.indexBoxInfo.top) / data.indexBoxInfo.childHeight));
  }

  /** 开始移动 */
  function touchmove(event: TouchEvent) {
    data.isTouch = true;
    const pageY = event.changedTouches[0]?.pageY || 0;
    goIndexView(Math.floor((pageY - data.indexBoxInfo.top) / data.indexBoxInfo.childHeight));
  }

  /** 触摸结束 */
  function touchend() {
    // 防止动画消失太快
    setTimeout(() => {
      data.isTouch = false;
    }, 300);
  }

  /** 跳转到对应视图 */
  function goIndexView(idx: number) {
    let index = idx;
    if (index === data.currentIndex) {
      return;
    }
    index = index > data.indexList.length - 1 ? data.indexList.length - 1 : index;
    index = index < 0 ? 0 : index;
    data.currentIndex = index;
    let height = 0;

    data.indexList.forEach((item, i) => {
      if (i <= index - 1) {
        height += item.height;
      }
    });
    data.scrollTop = height;
  }

  /** 滚动 */
  function onScroll(event: ScrollViewOnScrollEvent) {
    if (data.isTouch) {
      return;
    }
    let height = 0;
    let indexs = 0;
    data.indexList.forEach((item, index) => {
      height += item.height;
      if (event.detail.scrollTop >= height) {
        indexs = index + 1;
      }
    });
    data.currentIndex = indexs;
  }

  /** 初始化数据 */
  function initData() {
    const arr = Array.isArray(props.list) ? JSON.parse(JSON.stringify(props.list)) : [];
    const list: $IndexListChildItem[] = [];
    arr.forEach((item: unknown, index: number) => {
      const label = isObject(item) ? String(item[keyObj.value.labelKey] || '') : String(item);
      const value = isObject(item) ? item[keyObj.value.valueKey] || '' : item;
      list.push({
        label,
        value,
        index,
        pinyin: pinyin(String(label) || '', { pattern: 'first', toneType: 'none' }).toUpperCase()[0] || '',
        item,
      });
    });
    const indexList: $IndexListItem[] = [];
    for (let i = 0; i < 26; i++) {
      const letter = String.fromCharCode(65 + i);
      const obj: $IndexListItem = { title: letter, height: 0, top: 0, data: [] };
      list.forEach((item) => {
        if (item.pinyin === letter || item.pinyin === letter.toLowerCase()) {
          obj.data.push(item);
        }
      });
      if (props.showEmptyData || obj.data.length > 0) {
        indexList.push(obj);
      }
    }
    data.indexList = indexList;

    nextTick(() => {
      setTimeout(async () => {
        const indexBoxDomInfo = await getDomSize(`#${indexId}`, instance);
        const moveBoxInfo = await getDomSize(`#${bubbleBoxId}`, instance);
        data.indexBoxInfo = {
          childHeight: indexBoxDomInfo.height! / data.indexList.length || 0,
          top: indexBoxDomInfo.top || 0,
          moveBoxHeight: moveBoxInfo.height || 0,
        };
      });
    });
  }
</script>

<style lang="scss" scoped>
  .mxp-index-list {
    position: relative;

    .search-box {
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      align-items: stretch;
      justify-content: center;
      width: 100%;
      height: 60px;
      padding: 0 15px;
      border: 1px solid #eeeeee;
    }

    .scroll-view {
      position: relative;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      height: 100%;
      overflow: auto;
    }

    .index-box {
      position: absolute;
      top: 50%;
      right: 15px;
      z-index: 999;
      display: flex;
      flex-direction: column;
      gap: 5rpx;
      transform: translate(0, -50%);

      .index {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 20px;
        height: 20px;
        font-size: 12px;
        color: #606266;
        border-radius: 50%;
      }
    }

    .bubble-box {
      position: absolute;
      top: 0;
      right: 50px;
      z-index: 99;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 40px;
      height: 40px;
      background: #ffffff;
      border-radius: 100px 100px 0;
      opacity: 0;
      transition: all 0.2s;
      transform: rotate(-45deg);

      .text {
        font-size: 16px;
        transform: rotate(45deg);
      }
    }

    .show {
      opacity: 1;
    }
  }
</style>
